Skip to content

25. 辅助角色服务

25.1 关于辅助角色服务

.NET Core 3.0 新增了 Worker Service 的新项目模板,可以编写长时间运行的后台服务,并且能轻松的部署成 Windows服务Linux 守护程序

目前微软提供了两种方式创建长时间运行的后台服务:

  • 共宿主方式:中小型项目推荐,无需单独部署 Windows/Linux 服务
  • 独立 Worker Service 方式:需独立部署 Windows/Linux 服务

25.2 共宿主方式

共宿主方式指的是在现有的 Web 或其他应用程中创建类文件并派生自 BackgroundService 类即可。这种方式的典型特点就是和应用共生存周期,应用启动时启动,应用结束停止运行。

推荐等级推荐中小型项目使用这种方式。

using Microsoft.Extensions.Hosting;  
using System;  
using System.Threading;  
using System.Threading.Tasks;  

namespace YourPoject.Web.Core;  

public class Worker : BackgroundService  
{  
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        while (!stoppingToken.IsCancellationRequested)  
        {  
            Console.WriteLine(DateTime.Now);  

            // 延迟 1 秒  
            await Task.Delay(1000, stoppingToken);  
        }  
    }  
}  

之后在 Startup.cs 中注册即可:

services.AddHostedService<Worker>();  

25.2.1 最佳实践

最好的实践方式是创建独立的类库项目:YourProject.BackgroundServices,之后添加 YourPoject.ApplicationYourPoject.Core 层引用,将所有的 Worker 放在该层,同时创建 Startup.cs 类进行 Worker 统一注册,如:

using Microsoft.Extensions.DependencyInjection;  

namespace YourProject.BackgroundServices;  

public sealed class Startup : AppStartup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        services.AddHostedService<Worker>();  
        services.AddHostedService<Worker2>();  
    }  
}  

25.3 独立 Worker Service 方式

独立 Worker Service 方式的主要特点就是它是一个独立的项目,和现有的项目没有直接关联关系,需要分开独立部署

推荐等级推荐中大型项目使用这种方式,也就是独立部署成 Windows Service 或者 Linux 守护进程,具有独立生存周期,即使应用故障了也不会影响它的运行。

25.3.1 如何创建 Worker Service

通过 Visual Studio 2019 提供的 Worker Service 可直接创建。如图:

25.3.2 创建 Worker

当我们创建好 Worker Service 项目时,已经自带了一个 Worker 类并继承自 BackgroundService 基类。

Worker 正是我们辅助角色的主要工作类,在这里我们编写我们所有的业务逻辑。通常 Worker 默认格式为:

using Microsoft.Extensions.Hosting;  
using Microsoft.Extensions.Logging;  
using System;  
using System.Threading;  
using System.Threading.Tasks;  

namespace FurionWorkers  
{  
    public class Worker : BackgroundService  
    {  
        private readonly ILogger<Worker> _logger;  

        public Worker(ILogger<Worker> logger)  
        {  
            _logger = logger;  
        }  

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
        {  
            while (!stoppingToken.IsCancellationRequested)  
            {  
                _logger.LogInformation("Worker running at: {time}", DateTime.Now);  
                await Task.Delay(1000, stoppingToken);  
            }  
        }  
    }  
}  

当我们创建了 Worker 类之后,需要在 Program.cs 中进行注册,如:

using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace FurionWorkers  
{  
    public class Program  
    {  
        public static void Main(string[] args)  
        {  
            CreateHostBuilder(args).Build().Run();  
        }  

        public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .ConfigureServices((hostContext, services) =>  
                {  
                    services.AddHostedService<Worker>();  
                });  
    }  
}  

小知识如果使用了 Furion 包后可实现自动注册,请同时确保 Worker 声明为 public 级别。

25.3.3 多个 Worker

Worker Service 是支持定义多个 Worker 进行协调工作的,每个 Worker 是完全独立的工作环境,但可共享同一主进程信息。

25.3.4 生命周期

Worker ServiceWorker 提供了三个执行方法,分别代表三个生命周期:

  • StartAsync:负责启动 Worker Service,如果调用 StartAsync 方法的线程被一直阻塞了,那么 Worker Service 的启动就一直完成不了
  • ExecuteAsyncWorker Service 真正实现业务逻辑的地方,这里不能调用阻塞代码!!!
  • StopAsync:负责结束 Worker Service,如果调用 StopAsync 方法的线程被一直阻塞了,那么 Worker Service 的结束就一直完成不了
using Microsoft.Extensions.Hosting;  
using Microsoft.Extensions.Logging;  
using System;  
using System.Threading;  
using System.Threading.Tasks;  

namespace FurionWorkers  
{  
    public class Worker : BackgroundService  
    {  
        private readonly ILogger<Worker> _logger;  

        public Worker(ILogger<Worker> logger)  
        {  
            _logger = logger;  
        }  

        // 启动  
        public override Task StartAsync(CancellationToken cancellationToken)  
        {  
            return base.StartAsync(cancellationToken);  
        }  

        // 执行逻辑  
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
        {  
            while (!stoppingToken.IsCancellationRequested)  
            {  
                _logger.LogInformation("Worker running at: {time}", DateTime.Now);  
                await Task.Delay(1000, stoppingToken);  
            }  
        }  

        // 停止  
        public override Task StopAsync(CancellationToken cancellationToken)  
        {  
            return base.StopAsync(cancellationToken);  
        }  
    }  
}  

25.3.5 集成 Furion

Worker Service 集成 Furion 非常方便,只需要安装 Furion 的包即可,并在 Program.cs 中调用 .Inject() 方法,如:

using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace FurionWorkers  
{  
    public class Program  
    {  
        public static void Main(string[] args)  
        {  
            CreateHostBuilder(args).Build().Run();  
        }  

        public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .Inject()  
                .ConfigureServices((hostContext, services) =>  
                {  
                    // 以下代码可不用编写,Furion 已实现自动注册 Worker;  
                    // services.AddHostedService<Worker>();  
                });  
    }  
}  

默认情况下,Inject() 方法注册了 日志、缓存、依赖注入、加载配置、自定义 Startup 功能。

小知识集成 Furion 后会自动扫描 Worker 类并实现自动注册。

25.3.6 注册服务

Worker Service 注册服务和 Web 略有不同,Web 主要在 Starup.cs 类中注册,Worker ServiceProgram.cs 启动类的 ConfigureServices 方法中注册,如:

using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace FurionWorkers  
{  
    public class Program  
    {  
        public static void Main(string[] args)  
        {  
            CreateHostBuilder(args).Build().Run();  
        }  

        public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .Inject()  
                .ConfigureServices((hostContext, services) =>  
                {  
                    // 注册数据库服务  
                    services.AddDatabaseAccessor(options =>  
                    {  
                        options.AddDb<DefaultDbContext>();  
                    });  

                    // 注册远程请求  
                    services.AddRemoteRequest();  

                    // 等等其他服务注册  
                });  
    }  
}  

25.4 实现简单定时任务

小建议强烈建议使用 【26.1 调度作业】 章节内容实现强大的分布式定时任务。

Furion 框架为 BackgroundService 提供了定时任务的支持。

25.4.1 间隔执行方式

namespace WorkerService;  

public class Worker : BackgroundService  
{  
    private readonly ILogger<Worker> _logger;  

    private const int delay = 1000;  

    public Worker(ILogger<Worker> logger)  
    {  
        _logger = logger;  
    }  

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        while (!stoppingToken.IsCancellationRequested)  
        {  
            await Task.Delay(delay, stoppingToken);  

            var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);  
            await taskFactory.StartNew(async () =>  
            {  
                // 你的业务代码写到这里面  

                _logger.LogInformation("Worker running at: {time}", DateTime.Now);  

                await Task.CompletedTask;  

            }, stoppingToken);  
        }  
    }  
}  

25.4.2 Cron 表达式执行方式

小知识如需了解 Cron 表达式内容,可查阅 【26.2 Cron 表达式】 章节内容。

using Furion.TimeCrontab;  

namespace WorkerService;  

public class Worker : BackgroundService  
{  
    private readonly ILogger<Worker> _logger;  

    private readonly Crontab _crontab;  

    public Worker(ILogger<Worker> logger)  
    {  
        _logger = logger;  
        _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);  
    }  

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        while (!stoppingToken.IsCancellationRequested)  
        {  
            await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken);  

            var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);  
            await taskFactory.StartNew(async () =>  
            {  
                // 你的业务代码写到这里面  

                _logger.LogInformation("Worker running at: {time}", DateTime.Now);  

                await Task.CompletedTask;  
            }, stoppingToken);  
        }  
    }  
}  

BackgroundService 方式实现定时任务注意事项通过这种方式只是简单的实现定时任务,但\~\~不能对线程和时间进行精准控制,可能存在一些不执行或者重复执行等问题\~\~。

所以,强烈建议使用 【26.1 调度作业】 章节内容实现强大的分布式定时任务。

25.4.3 实现 串行 操作

默认情况下,定时任务都是采用 并行 的方式,也就是不会等待上一次任务完成,如果需要等待上一次任务完成,可以修改为 串行 方式:

using Furion.TimeCrontab;  

namespace WorkerService;  

public class Worker : BackgroundService  
{  
    private readonly ILogger<Worker> _logger;  

    private readonly Crontab _crontab;  

    private bool _isLock = false;  

    public Worker(ILogger<Worker> logger)  
    {  
        _logger = logger;  
        _crontab = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);  
    }  

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        while (!stoppingToken.IsCancellationRequested)  
        {  
            if (_isLock) goto next;  
            _isLock = true;  

            var taskFactory = new TaskFactory(System.Threading.Tasks.TaskScheduler.Current);  
            var task = await taskFactory.StartNew(async () =>  
            {  
                // 模拟耗时操作  
                await Task.Delay(2000);  

                _logger.LogInformation("Worker running at: {time}", DateTime.Now);  

                await Task.CompletedTask;  
            }, stoppingToken);  

            // 等待任务完成  
            await task.ContinueWith(task => _isLock = false);  

        next:  
            await Task.Delay(_crontab.GetSleepTimeSpan(DateTime.Now), stoppingToken);  
        }  
    }  
}  

25.5 依赖注入使用

Worker Service 只为 Worker 提供了单例作用域的服务注入,如果需要注入瞬时或作用域对象,需手动创建作用域,如:

public class Worker : BackgroundService  
{  
    // 日志对象  
    private readonly ILogger<JobService> _logger;  

    // 服务工厂  
    private readonly IServiceScopeFactory _scopeFactory;  
    public Worker(ILogger<Worker> logger  
        , IServiceScopeFactory scopeFactory)  
    {  
        _logger = logger;  
        _scopeFactory = scopeFactory;  
    }  

    protected override Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        // 放在循环外可以避免高频下频繁创建作用域和解析服务  
        using var scope = _scopeFactory.CreateScope();  
        var services = scope.ServiceProvider;  

        while (!stoppingToken.IsCancellationRequested)  
        {  
            // 放在循环内针对频率不是很高的操作  
            // using var scope = _scopeFactory.CreateScope();  
            // var services = scope.ServiceProvider;  

            // 获取数据库上下文  
            var dbContext = Db.GetDbContext(services);  
            // 获取仓储  
            var respository = Db.GetRepository<Person>(services);  
            // 解析其他服务  
            var otherService = services.GetService<XXX>();  
        }  

        return Task.CompletedTask;  
    }  
}  

25.6 如何部署

25.6.1 共宿主方式

共宿主方式方式部署非常简单,只需要部署所在的 Web 或其他应用程序项目即可,服务将随着项目启动而启动。

特别说明如果部署到 IIS 中,可能存在 Worker Service 被回收的情况,可参考 【34.1.5 IIS 回收配置

25.6.2 独立 Worker Service 方式

Worker Service 支持部署到 Windows Service 中 或 Linux 守护进程中


部署到 Windows Service

推荐使用新方式推荐查看 【34.8 Windows Service 部署】 文档替代以下小节内容。

  • 第一步:安装 Microsoft.Extensions.Hosting.WindowsServices 拓展包
  • 第二步:在 Program.cs 中添加 .UseWindowsService()
using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace FurionWorkers  
{  
    public class Program  
    {  
        public static void Main(string[] args)  
        {  
            CreateHostBuilder(args).Build().Run();  
        }  

        public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .UseWindowsService()  
                .Inject()  
                .ConfigureServices((hostContext, services) =>  
                {  
                    // 以下代码可不用编写,Furion 已实现自动注册 Worker;  
                    // services.AddHostedService<Worker>();  
                });  
    }  
}  

  • 第三步:发布 Worker Service,可通过 dotnet publish -c Release -o C:\FurionWorker 命令发布或通过 Visual Studio 2019 发布。

独立发布不依赖 SDK 方式 dotnet publish -c release -r win10-x64 --framework net6.0

  • 第四步:通过 sc.exe 工具来管理并创建 Windows 服务,通过 管理员模式 并打开控制台,输入:
sc.exe create FurionWorkerServices binPath= C:\FurionWorker\FurionWorker.exe  

注意=后面要有一个空格 创建成功后可通过 sc.exe query FurionWorkerServices 查看服务状态。

  • 第五步

启动服务:sc.exe start FurionWorkerServices,启动之后就可以在 Windows 服务工具中查看了。

停止服务:sc.exe stop NETCoreDemoWorkerService

删除服务:sc.exe delete NETCoreDemoWorkerService

特别提醒以上所有 sc.exe 命令必须在 管理员 模式下进行。 sc.exe delete NETCoreDemoWorkerService, 执行删除时候, 把Windows 服务工具关闭, 否则, 电脑重启后才会显示删除;


部署到 Linux 守护程序

  • 第一步:安装 Microsoft.Extensions.Hosting.Systemd 拓展包
  • 第二步:在 Program.cs 中添加 .UseSystemd()
using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Hosting;  

namespace FurionWorkers  
{  
    public class Program  
    {  
        public static void Main(string[] args)  
        {  
            CreateHostBuilder(args).Build().Run();  
        }  

        public static IHostBuilder CreateHostBuilder(string[] args) =>  
            Host.CreateDefaultBuilder(args)  
                .UseSystemd()  
                .Inject()  
                .ConfigureServices((hostContext, services) =>  
                {  
                    // 以下代码可不用编写,Furion 已实现自动注册 Worker;  
                    // services.AddHostedService<Worker>();  
                });  
    }  
}  

部署到 Linux 守护进程 就是这么简单。

25.7 关于 Windows 部署日志问题

推荐使用新方式根据 【34.8 Windows Service 部署】 文档内容部署可避免以下问题。

默认情况下,使用 Windows Services 部署后,日志文件可能会在系统盘的 System32 下。

note注意:如果使用了 Furion 提供的 AddFileLogging 日志模块,可尝试添加 AppDomain.CurrentDomain.BaseDirectory 作为文件日志写入目录。

25.8 反馈与建议

与我们交流给 Furion 提 Issue